package risoe.syslab.fhnew.hw;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;

import risoe.syslab.fhnew.common.SystemConstants.DeviceType;
import risoe.syslab.fhnew.config.ActuatorConfig;
import risoe.syslab.fhnew.config.EnoceanInterfaceConfig;
import risoe.syslab.fhnew.config.HWInterfaceConfig;
import risoe.syslab.fhnew.config.SensorConfig;
import risoe.syslab.fhnew.hw.enocean.DoorContactMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanErrorMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanException;
import risoe.syslab.fhnew.hw.enocean.EnoceanMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanParser;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRIDRANGEMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRMODEMDUPIDMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRMODEMNACKMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRMODEMNWACKMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRSYNTAXCSUMMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRSYNTAXHSEQMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRSYNTAXLENMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRSYNTAXORGMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTERRTXIDRANGEMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTINFIDBASEMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTINFINITMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTINFMODEMSTATEMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTINFRXSENSMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTINFSWVERMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRMTOKMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanRRTMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanReceiver;
import risoe.syslab.fhnew.hw.enocean.EnoceanTCTRESETMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanTCTSETRXSENSMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanTRTRPSMessage;
import risoe.syslab.fhnew.hw.enocean.EnoceanUtils;
import risoe.syslab.fhnew.hw.enocean.LightMessage;
import risoe.syslab.fhnew.hw.enocean.IndoorTemperatureMessage;
import risoe.syslab.fhnew.hw.enocean.MotionDetectorMessage;
import risoe.syslab.fhnew.hw.enocean.PushButtonMessage;
import risoe.syslab.model.CompositeMeasurement;

public class EnoceanInterface extends HardwareInterface implements EnoceanParser, ConnectionManagerInterface {
	// Constants
	private static final int RESET_TIMEOUT = 1; // Hours
	private static final int SETUP_TIMEOUT = 1000; // Milliseconds
	private static final int BAUD_RATE = 9600;
	
	// Mappings
	private HashMap<Integer, Class<?>> mAddressMap;
	private HashMap<Integer, String> mDeviceNameMap;
	private HashMap<String, Integer> mNameToAddrMap;
	private HashMap<String, Integer> mChannelMap;

	private SerialPort mSerialPort;
	private String mSerialAddress;
	private EnoceanReceiver mReceiver;
	private char[] orgBytes;
	private ArrayList<EnoceanMessage> setupExpect;
	private boolean setupState;
	private ConnectionManager mConnectionManager;
	private ScheduledExecutorService mExecutor;

	private static final Class<?>[] replyTypes={
		EnoceanRMTINFINITMessage.class,
		EnoceanRMTOKMessage.class,
		EnoceanRMTERRMessage.class,
		EnoceanRMTINFIDBASEMessage.class,
		EnoceanRMTINFRXSENSMessage.class,
		EnoceanRMTINFMODEMSTATEMessage.class,
		EnoceanRMTINFSWVERMessage.class,
		EnoceanRMTERRMODEMNWACKMessage.class,
		EnoceanRMTERRMODEMNACKMessage.class,
		EnoceanRMTERRMODEMDUPIDMessage.class,
		EnoceanRMTERRSYNTAXHSEQMessage.class,
		EnoceanRMTERRSYNTAXLENMessage.class,
		EnoceanRMTERRSYNTAXCSUMMessage.class,
		EnoceanRMTERRSYNTAXORGMessage.class,
		EnoceanRMTERRTXIDRANGEMessage.class,
		EnoceanRMTERRIDRANGEMessage.class
	};

	public EnoceanInterface(HWInterfaceConfig hic) {
		super(hic);
		mSerialAddress = ((EnoceanInterfaceConfig)hic).getSerialAddress();
	}

	@Override
	protected void startInterfaceImpl(ArrayList<SensorConfig> sensors, ArrayList<ActuatorConfig> actuators) {
		mAddressMap = new HashMap<Integer, Class<?>>(sensors.size()+actuators.size());
		mDeviceNameMap = new HashMap<Integer, String>(sensors.size()+actuators.size());
		mNameToAddrMap = new HashMap<String, Integer>(sensors.size()+actuators.size());
		mChannelMap = new HashMap<String, Integer>(sensors.size()+actuators.size());
		setupExpect=new ArrayList<EnoceanMessage>();

		// Map
		for (SensorConfig sensor : sensors){
			try {
				int addr = (int) Long.parseLong(sensor.getInterfaceAttributes().get("addr"), 16);
				if (sensor.getType() == DeviceType.Button){
					mAddressMap.put(addr, PushButtonMessage.class);
				}
				else if (sensor.getType() == DeviceType.Temperature){
					mAddressMap.put(addr, IndoorTemperatureMessage.class);
				}
				else if (sensor.getType() == DeviceType.Brightness){
					mAddressMap.put(addr, LightMessage.class);
				}
				else if (sensor.getType() == DeviceType.Insolation){
					mAddressMap.put(addr, LightMessage.class);
				}
				else if (sensor.getType() == DeviceType.Motion){
					mAddressMap.put(addr, MotionDetectorMessage.class);
				}
				else  if (sensor.getType() == DeviceType.Door || sensor.getType() == DeviceType.Window){
					mAddressMap.put(addr, DoorContactMessage.class);
				}
				else{
					System.out.println(getName()+" error: unknown enocean type "+sensor.getType());
					getLogger().logEvent(getName(), "Error", "Unknown Enocean type: "+sensor.getType()+", for sensor: "
							+sensor.getName());
				}

				mDeviceNameMap.put(addr, sensor.getName());
				mChannelMap.put(sensor.getName(), Integer.valueOf(sensor.getInterfaceAttributes().get("channel")));
				mNameToAddrMap.put(sensor.getName(), addr);
			} catch (NumberFormatException e) {
				// addresses can also be ip numbers (as dotted decimal string)
				System.out.println(getName()+" error mapping "+sensor.getName());
				getLogger().logEvent(getName(), "Error", "Address format error for sensor: " + sensor.getName());
			}
		}

		for (ActuatorConfig actuator : actuators){
			try{
				int addr = (int) Long.parseLong(actuator.getInterfaceAttributes().get("addr"), 16);
				mNameToAddrMap.put(actuator.getName(), addr);
				mChannelMap.put(actuator.getName(), Integer.valueOf(actuator.getInterfaceAttributes().get("channel")));
			} catch (NumberFormatException e) {
				// addresses can also be ip numbers (as dotted decimal string)
				System.out.println(getName()+" error mapping "+actuator.getName());
				getLogger().logEvent(getName(), "Error", "Address format error for sensor: " + actuator.getName());
			}
		}

		orgBytes=new char[replyTypes.length];
		char[] dummybuf=new char[11];
		for (int i=0;i<replyTypes.length;i++) {
			try {
				Constructor<?> c=replyTypes[i].getConstructor(new Class[]{char[].class});
				EnoceanMessage e=(EnoceanMessage)c.newInstance(dummybuf);
				orgBytes[i]=(e.getDataPacket())[3];
			}
			catch (NoSuchMethodException e) {e.printStackTrace();}
			catch (InstantiationException e) {e.printStackTrace();}
			catch (IllegalAccessException e) {e.printStackTrace();}
			catch (InvocationTargetException e) {e.printStackTrace();}
		}

		getLogger().logEvent(getName(), "Initialization", "Complete");

		// Set up the connection
		mConnectionManager = new ConnectionManager(this);
		mConnectionManager.startConnection();
	}

	private int blockExpect(long timeout) {
		long t=System.currentTimeMillis();
		while (!setupExpect.isEmpty()) {
			try {
				Thread.sleep(20);
			}
			catch (InterruptedException e) { /* ignore exception! */ };
			if ((System.currentTimeMillis()-t)>timeout) {
				return -1;
			}
		}
		return 0;
	}

	public void sendMessages(EnoceanMessage[] messages){
		synchronized (this) {
			for (EnoceanMessage msg : messages){
				sendMessage(msg);
			}
		}
	}

	public void sendMessage(EnoceanMessage e) {
		if (!mConnectionManager.isConnected() && !setupState){
			getLogger().logEvent(getName(), "Error", "Interface not connected - Error applying actuation");
			return;
		}

		if (EnoceanUtils.DEBUG) {
			System.out.print("sendMessage --- ");
		}
		try {
			char[] dp=e.getDataPacket();
			for (int i=0;i<dp.length;i++) {
				mSerialPort.writeByte((byte)dp[i]);
			}
			if (EnoceanUtils.DEBUG) {
				System.out.println( e.getDataPacketAsString() );
			}
		}
		catch (SerialPortException ex) {
			getLogger().logEvent(getName(), "Error", "Failed to send message to device: "+e.getStringAddress());
		}
	}

	@Override
	protected void stopInterfaceImpl() {
		mConnectionManager.stopConnection();
	}

	@Override
	public boolean connect() {
		try {
			// Find the correct port and open a connection
			mSerialPort = new SerialPort(mSerialAddress);
			mSerialPort.openPort();

			// Initialize the serial port
			mSerialPort.setParams(BAUD_RATE, SerialPort.DATABITS_8,
					SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);

			// Listen for events
			mReceiver = new EnoceanReceiver(this, mSerialPort);
			mSerialPort.addEventListener(new SerialPortEventListener() {

				@Override
				public void serialEvent(SerialPortEvent event) {
					if (event.isRXCHAR()){
						if (event.getEventValue() >= 1){
							mReceiver.serialEvent(event);
						}
					}
					else if (event.isERR()){
						mConnectionManager.onDisconnected();
					}
				}
			}, SerialPort.MASK_RXCHAR | SerialPort.MASK_ERR);

			setupState = true;
			// Attempt to set RX sensitivity to high
			setupExpect.clear();
			setupExpect.add(new EnoceanRMTOKMessage(new char[11]));
			sendMessage(new EnoceanTCTSETRXSENSMessage(EnoceanTCTSETRXSENSMessage.SENSITIVITY_HIGH));
			int be = blockExpect(SETUP_TIMEOUT);
			if (be==-1) {
				getLogger().logEvent(getName(), "Error", "RX sensitivity could not be set.");
			} else {
				getLogger().logEvent(getName(), "Note", "RX sensitivity set to high.");
			}
			setupState = false;
			
			// Start periodic reset of transceiver
			mExecutor = Executors.newSingleThreadScheduledExecutor();
			mExecutor.schedule(new Runnable() {
				@Override
				public void run() {
					// Reset hardware and reschedule self
					resetTransceiver();
					mExecutor.schedule(this, RESET_TIMEOUT, TimeUnit.HOURS);
				}
			}, SETUP_TIMEOUT, TimeUnit.MILLISECONDS);

			// Report the succesful connection
			return true;
		} catch (SerialPortException e){
			if (mReceiver != null){
				mReceiver.stop();
				mReceiver = null;
			}
			getLogger().logEvent(getName(), "Connection", "Error connecting: "+e.getMessage());
		}

		return false;
	}

	public void disconnect() {
		// Stop periodical reset of transceiver
		mExecutor.shutdownNow();
		mExecutor = null;
		
		// End serial eventing and close port
		if (mSerialPort != null){
			try {
				mSerialPort.removeEventListener();
				mSerialPort.closePort();
			} catch (SerialPortException e1) {
				e1.printStackTrace();
			}
			mSerialPort = null;
		}
		if (mReceiver != null){
			mReceiver.stop();
			mReceiver = null;
		}
	}

	public boolean probeConnection() {
		// No way to check connection
		return false;
	}

	/**
	 * Used to perform a blocking reset of the Enocean transceiver in order to avoid slowdown over time
	 */
	private void resetTransceiver() {
		synchronized (this) {
			// Attempt to reset the transceiver
			setupState = true;
			setupExpect.clear();
			for (int i=0;i<15;i++) {
				setupExpect.add(new EnoceanRMTINFINITMessage(new char[11]));
			}
			sendMessage(new EnoceanTCTRESETMessage());
			int be=blockExpect(SETUP_TIMEOUT);
			if (be==-1) {
				getLogger().logEvent(getName(), "Error", "Enocean transceiver reset failed.");
			} else {
				getLogger().logEvent(getName(), "Note", "Enocean transceiver reset success.");
			}
			setupState = false;
		}
	}

	public void messageReceived(EnoceanMessage msg) {
		if (msg instanceof EnoceanRRTMessage) { //the only ones containing sensor data
			EnoceanRRTMessage rmsg=(EnoceanRRTMessage)msg;

			// Get the name of the appropriate sensor
			int channel = msg.getChannel(); // channel info for switches..
			String name = mDeviceNameMap.get(rmsg.getPhysicalAddress());
			if (name != null){

				// This is the sensor we are interested in updating
				CompositeMeasurement v=rmsg.getValue(channel);
				if (!Double.isNaN(v.value)) { //NaN is currently used for pushbutton release messages - we'll just ignore those.
					mData.clear();
					mData.put(name, v);
					notifyProcessImage();
				}

			}
			else{
				System.out.println("WARNING: No device found for address " + msg.getStringAddress() + ", channel " + channel );
			}
		}
	}    

	public void sentMessageAcknowledged(EnoceanMessage e) {
		if (e instanceof EnoceanRRTMessage) { //the only ones containing sensor data
			EnoceanRRTMessage rmsg=(EnoceanRRTMessage)e;
			String name = mDeviceNameMap.get(rmsg.getPhysicalAddress());
			if (name != null){
				System.out.println(getName()+" ack from "+name);
			}
		}
		//		synchronized (sentCommandSynch) {
		//			if (sentCommand != null) {
		//				String address = sentCommand.getMessage().getStringAddress();
		//				String room = house.getRoomFromAddress(address);
		//
		//				// update the state value into the house data structure
		//				if (sentCommand.getNewState() != null) {
		//					// Right now, we only have boolean responses to sendMessage()
		//					house.updateState(
		//							room, sentCommand.getNewState().booleanValue(), 
		//							address, sentCommand.getMessage().getChannel() );
		//					appTraceCallback(
		//							"ACK "+ sentCommand.getMessage() +
		//							":device state update with new state");
		//				} else {
		//					appTraceCallback("normal ACK");
		//				}
		//				enoceanMonitor.setAvailability(true);
		//
		//				new_houseState.setAvailability(true);
		//				sentCommand = null;
		//			} else {
		//				enoceanMonitor.setAvailability(true);
		//			}
		//		}
	}

	public void sentMessageFailure(EnoceanException e) {
		getLogger().logEvent(getName(), "Error", "Error sending message "+e.getMessage());
		System.out.println(getName()+" exception: "+e.getMessage());

		/*enoceanMonitor.setAvailability(true);
    synchronized (sentCommandSynch) {
      // TODO add the message again to the commands list?
      sentCommand = null;
    }*/
	}

	@Override
	public boolean isConnected() {
		return mConnectionManager.isConnected();
	}

	@Override
	public void applyActuation(Actuator actuator) {
		Integer addr = mNameToAddrMap.get(actuator.getName());
		if (addr != null){
			if (actuator.getType() == DeviceType.PowerControlledHeater ||
					actuator.getType() == DeviceType.DimmableLight || 
					actuator.getType() == DeviceType.Waterboiler ){
				// Get the setpoint
				boolean on = actuator.getSetpoint().value > 0.5? true: false;

				// Get the channel
				int channel = mChannelMap.get(actuator.getName());

				// Construct a down/up and a release message and transmit them
				EnoceanMessage action = new EnoceanTRTRPSMessage(addr, channel, on? EnoceanTRTRPSMessage.UP : EnoceanTRTRPSMessage.DOWN);
				EnoceanMessage release = new EnoceanTRTRPSMessage(addr, channel, EnoceanTRTRPSMessage.RELEASE);
				sendMessages(new EnoceanMessage[]{action, release});
			} 
			else{
				getLogger().logEvent(getName(), "Error", "Attempted actuation of non-actuator type: " + 
						actuator.getName() + ", " + String.format("0x%08x", addr));
			}
		}
		else{
			getLogger().logEvent(getName(), "Error", "No address mapped for actuator " + actuator.getName());
		}
	}


	@Override
	public void parseRawMessage(char[] buf) {
		int hseq=buf[0]&((char)0xe0);
		int org=buf[1];
		int id=(((int)buf[6])<<24)+
				(((int)buf[7])<<16)+
				(((int)buf[8])<<8)+
				((int)buf[9]);

		if (EnoceanUtils.DEBUG) {
			System.out.print( "received message: " );
			for (int i = 0; i < buf.length; i++) {
				if (i>0 && (i%2)==0) System.out.print(" ");
				System.out.print( String.format( "%02x", (int) buf[i] ));
			}
			System.out.print( "; id=" + String.format( "%08x", id ) );
			System.out.println( "; hseq=" + String.format( "%02x", hseq ) );
		}

		EnoceanMessage decoded=null;
		switch (hseq) {
		case EnoceanUtils.HSEQ_RRT:
			Class<?> clazz = mAddressMap.get(id);
			if (clazz != null){
				// This is a known sensor. Handle appropriately
				try {
					Constructor<?> c =
							clazz.getConstructor( new Class[]{ char[].class } );
					decoded = (EnoceanMessage) c.newInstance(buf);
				}
				catch (NoSuchMethodException e) {e.printStackTrace();}
				catch (InstantiationException e) {e.printStackTrace();}
				catch (IllegalAccessException e) {e.printStackTrace();}
				catch (InvocationTargetException e) {e.printStackTrace();}
			}
			else {
				System.out.println("Unknown sensor: " + String.format("0x%08x", id));
				getLogger().logEvent(getName(), "Error", "Unkown sensor: " + String.format("0x%08x", id));
			}

			if (decoded!=null) {
				messageReceived(decoded);
			}
			break;
		case EnoceanUtils.HSEQ_RMT:
			for (int i=0;(decoded==null) && (i<replyTypes.length);i++) {
				if (orgBytes[i]==((char)org)) {
					try {
						Constructor<?> c=replyTypes[i].getConstructor(
								new Class[]{char[].class});
						decoded=(EnoceanMessage)c.newInstance(buf);
					}
					catch (NoSuchMethodException e) {e.printStackTrace();}
					catch (InstantiationException e) {e.printStackTrace();}
					catch (IllegalAccessException e) {e.printStackTrace();}
					catch (InvocationTargetException e) {e.printStackTrace();}
				}
			}
			if (EnoceanUtils.DEBUG) {
				System.out.println("MSG: "+((decoded==null)?"unknown":decoded));
			}
			if (setupState) {
				if (setupExpect.size()>0) {
					if (decoded.getClass().equals(setupExpect.get(0).getClass())) {
						setupExpect.remove(0);
					}
				}
			}
			else { //!initState, normal operation
				if (decoded!=null) {
					if (decoded instanceof EnoceanRMTOKMessage) {
						sentMessageAcknowledged(decoded);
					}
					else if (decoded instanceof EnoceanErrorMessage) {
						sentMessageFailure(new EnoceanException(decoded));
					}
					else if (decoded instanceof EnoceanRMTMessage ) {
						messageReceived(decoded);
					}
				}
			}

			break;
		default:
			if (EnoceanUtils.DEBUG) {
				System.out.println("Unknown HSEQ: " + hseq + ", " + id );
			}
			break;
		}
	}
}
